package gfx.hdr;

import gfx.layers.Layer;
import gfx.layers.LayerManager;

import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;

import javax.swing.SwingUtilities;

import core.exceptions.ImageDimensionException;


public class ImageHDR extends Layer implements Runnable{
	private List<Exposure> exposures;
	private int width;
	private int height;
	private float[] hdrContent;
	private double exposure = 10;
	
	public ImageHDR(int width, int height, LayerManager layerManager){
		super("HDR Image", layerManager);
		setSelected(false);
		this.width = width;
		this.height = height;
		
		exposures = new ArrayList<Exposure>();
	}
	
	public void addExposure(Layer layer) throws ImageDimensionException{
		if(layer.getImage().getWidth() != width || layer.getImage().getHeight() != height)
			throw new ImageDimensionException("The added exposure doesn't match the expected dimension.");
		if(exposures.size() == Integer.SIZE)
			throw new IndexOutOfBoundsException("The maximum of 32 Exposures has been reached. No more exposures can be added.");
		
		exposures.add(new Exposure(layer, this));
		
	}

	@Override
  public void run() {
		if(hdrContent == null){
			hdrContent = new float[width * height * 3];
			calculateRelativeExposures();
			generateHdrContent();
			exposeImage(exposure);
			SwingUtilities.invokeLater(this);
		}
		else{
		  getLayerManager().addLayer(this);
		}
  }
	
	private void calculateRelativeExposures(){
		int exCnt = exposures.size();
	  int[] pixelsBitFlagged = new int[width * height];
	  
	  for(int i = 0; i < exCnt; i++){
	  	exposures.get(i).bitFlagExposureRelevantAreas(pixelsBitFlagged, i);
	  }
	  
	  int numFactors = (exCnt*exCnt - exCnt) / 2;
	  double[] differenceFactors = new double[numFactors];
	  int[] differenceMeasures = new int[numFactors];
	  
	  int bitMaskA, bitMaskB;
	  
	  int i, j, h, diffIndex;
	  for(i = 0; i < pixelsBitFlagged.length; i++){
	  	bitMaskA = 1;
	  	bitMaskB = 1;
	  	diffIndex = 0;
	  	for(j = 0; j < exCnt - 1; j++){
	  		for(h = j + 1; h < exCnt; h++){
	  			bitMaskB <<=1;
	  			if((pixelsBitFlagged[i] & bitMaskA) != 0 && 
	  				 (pixelsBitFlagged[i] & bitMaskB) != 0){
	  				differenceFactors[diffIndex] += (double)exposures.get(h).getLuminance(i) / (double)exposures.get(j).getLuminance(i);
	  				differenceMeasures[diffIndex]++;
	  			}
	  			diffIndex++;
	  		}
	  		bitMaskA <<= 1;
	  		bitMaskB = bitMaskA;
	  	}
	  }
	  
	  double[] measures = new double[exCnt];
	  double[] absLum = new double[exCnt];
	  int mInd = 0;
	  
	  measures[mInd] = 1;
	  absLum[mInd] = 1;
	  i = 0;
	  
	  for(j = 0; j < exCnt - 1; j++){
	  	absLum[j] /= (double)measures[j];
  		for(h = j + 1; h < exCnt; h++){
  			absLum[h] += absLum[j] * differenceFactors[i];
  			measures[h] += differenceMeasures[i];
  			i++;
  		}
	  }
	  absLum[exCnt - 1] /= (double)measures[exCnt - 1];
	  
	  for(i = 0; i < exCnt; i++){
	  	exposures.get(i).setRelativeExposure(absLum[i]);
	  }
	  System.out.println("Relative Exposures Calculation -done-");
	}
	
	private void generateHdrContent(){
		double[] relevances = new double[width * height];
		int color;
		double red, green, blue;
		boolean isPixelSet;
		for(int i = 0; i < width * height; i++){
			isPixelSet = false;
			for(Exposure e : exposures){
				double relevance = 1 - (((double)Math.abs(e.getLuminance(i) - 127) + 0.5) / 127);
				if(relevance > 0.05){
					isPixelSet = true;
					color = e.getImage().getRGB(i % width, i / width);
					red = (color & 0x00ff0000) >> 16;
					green = (color & 0x0000ff00) >> 8;
	  			blue =  color & 0x000000ff;
					hdrContent[i*3 + 0] += red * relevance / e.getRelativeExposure();
					hdrContent[i*3 + 1] += green * relevance / e.getRelativeExposure();
					hdrContent[i*3 + 2] += blue * relevance / e.getRelativeExposure();
					
					relevances[i] += relevance;
				}
			}
			if(!isPixelSet){
				color = exposures.get(0).getImage().getRGB(i % width, i / width);
				red = (color & 0x00ff0000) >> 16;
				green = (color & 0x0000ff00) >> 8;
  			blue =  color & 0x000000ff;
				hdrContent[i*3 + 0] += red / exposures.get(0).getRelativeExposure();
				hdrContent[i*3 + 1] += green / exposures.get(0).getRelativeExposure();
				hdrContent[i*3 + 2] += blue / exposures.get(0).getRelativeExposure();
				relevances[i] = 1;
			}
		}
		for(int i = 0; i < width * height; i++){
			hdrContent[i*3 + 0] /= relevances[i];
			hdrContent[i*3 + 1] /= relevances[i];
			hdrContent[i*3 + 2] /= relevances[i];
		}
		System.out.println("HDR Content Creation -done-");
	}
	
	public void setExposure(double exposure){
		this.exposure = exposure;
		exposeImage(exposure);
		getLayerManager().layerHasChanged(this);
	}
	
	public double getExposure(){
		return exposure;
	}
	
	private void exposeImage(double exposure){
		if(image == null)
			image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
		
		for(int i = 0; i < width * height; i++){
			int rgb = 0;
			
			rgb |= (int)Math.min(255, Math.max(0, (int)(hdrContent[i*3 + 0] * exposure))) << 16;
			rgb |= (int)Math.min(255, Math.max(0, (int)(hdrContent[i*3 + 1] * exposure))) << 8;
			rgb |= (int)Math.min(255, Math.max(0, (int)(hdrContent[i*3 + 2] * exposure)));
			
			image.setRGB(i % width, i / width, rgb);
		}
	}
	
	public int getWidth(){
		return width;
	}
	
	public int getHeight(){
		return height;
	}
}
